// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.linqingying.gitcode

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState

import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.VcsException
import com.intellij.openapi.vcs.VcsNotifier
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.mapSmartSet
import com.intellij.vcsUtil.VcsFileUtil
import com.linqingying.gitcode.api.GitCodeApiRequestExecutor
import com.linqingying.gitcode.api.GitCodeApiRequests
import com.linqingying.gitcode.api.data.request.Type
import com.linqingying.gitcode.api.util.GitCodeApiPagesLoader
import com.linqingying.gitcode.authentication.GCAccountsUtil
import com.linqingying.gitcode.authentication.accounts.GCAccountManager
import com.linqingying.gitcode.authentication.accounts.GitCodeAccount
import com.linqingying.gitcode.authentication.accounts.GitCodeAccountInformationProvider
import com.linqingying.gitcode.exceptions.GitCodeMissingTokenException
import com.linqingying.gitcode.i18n.GitCodeBundle
import com.linqingying.gitcode.ui.GitCodeShareDialog
import com.linqingying.gitcode.ui.dialog.GitCodeExistingRemotesDialog
import com.linqingying.gitcode.ui.dialog.GitCodeUntrackedFilesDialog
import com.linqingying.gitcode.util.*
import git4idea.DialogManager
import git4idea.GitUtil
import git4idea.actions.GitInit
import git4idea.commands.Git
import git4idea.commands.GitCommand
import git4idea.commands.GitLineHandler
import git4idea.i18n.GitBundle
import git4idea.remote.hosting.findKnownRepositories
import git4idea.repo.GitRepository
import git4idea.util.GitFileUtils
import kotlinx.coroutines.runBlocking
import java.awt.Component
import java.io.IOException
@RequiresBlockingContext
fun <T> invokeAndWaitIfNeeded(modalityState: ModalityState? = null, runnable: () -> T): T {
    val app = ApplicationManager.getApplication()
    if (app.isDispatchThread) {
        return runnable()
    }
    else {
        var resultRef: T? = null
        app.invokeAndWait({ resultRef = runnable() }, modalityState ?: ModalityState.defaultModalityState())
        @Suppress("UNCHECKED_CAST")
        return resultRef as T
    }
}

object GCShareProjectUtil {
    private val LOG = GitCodeUtil.LOG

    // get gitRepository
    // check for existing git repo
    // check available repos and privateRepo access (net)
    // Show dialog (window)
    // create GitCode repo (net)
    // create local git repo (if not exist)
    // add GitCode as a remote host
    // make first commit
    // push everything (net)
    @JvmStatic
    fun shareProjectOnGitCode(project: Project, file: VirtualFile?) {
        FileDocumentManager.getInstance().saveAllDocuments()

        val gitRepository = GitCodeGitHelper.findGitRepository(project, file)
        val possibleRemotes = gitRepository
            ?.let(project.service<GCHostedRepositoriesManager>()::findKnownRepositories)
            ?.map { it.remote.url }.orEmpty()
        if (possibleRemotes.isNotEmpty()) {
            val existingRemotesDialog = GitCodeExistingRemotesDialog(project, possibleRemotes)
            DialogManager.show(existingRemotesDialog)
            if (!existingRemotesDialog.isOK) {
                return
            }
        }

        val progressManager = service<ProgressManager>()
        val accountInformationProvider = GitCodeAccountInformationProvider.getInstance()
        val gitHelper = GitCodeGitHelper.getInstance()
        val git = Git.getInstance()

        val accountInformationLoader = object : (GitCodeAccount, Component) -> Pair<Boolean, Set<String>> {
            private val loadedInfo = mutableMapOf<GitCodeAccount, Pair<Boolean, Set<String>>>()

            @Throws(IOException::class)
            override fun invoke(account: GitCodeAccount, comp: Component) = loadedInfo.getOrPut(account) {
                loadEnsuringTokenExistsToken(account, comp)
            }

            private fun loadEnsuringTokenExistsToken(
                account: GitCodeAccount,
                comp: Component
            ): Pair<Boolean, Set<String>> {
                while (true) {
                    try {
                        return progressManager.runProcessWithProgressSynchronously(ThrowableComputable<Pair<Boolean, Set<String>>, IOException> {
                            val token = runBlocking {
                                service<GCAccountManager>().findCredentials(account)
                                    ?: throw GitCodeMissingTokenException(account)
                            }
                            val requestExecutor =
                                GitCodeApiRequestExecutor.Factory.getInstance().create(account.server, token)

                            val user = requestExecutor.execute(
                                progressManager.progressIndicator,
                                GitCodeApiRequests.CurrentUser.get(account.server)
                            )
                            val names = GitCodeApiPagesLoader
                                .loadAll(
                                    requestExecutor, progressManager.progressIndicator,
                                    GitCodeApiRequests.CurrentUser.Repos.pages(account.server, Type.OWNER)
                                )
                                .mapSmartSet { it.name }
                            user.canCreatePrivateRepo() to names
                        }, GitCodeBundle.message("share.process.loading.account.info", account), true, project)
                    } catch (mte: GitCodeMissingTokenException) {
                        GCAccountsUtil.requestNewToken(account, project, comp) ?: throw mte
                    }
                }
            }
        }

        val shareDialog = GitCodeShareDialog(
            project,
            gitRepository?.remotes?.map { it.name }?.toSet() ?: emptySet(),
            accountInformationLoader
        )
        DialogManager.show(shareDialog)
        if (!shareDialog.isOK) {
            return
        }

        val name: String = shareDialog.getRepositoryName()
        val isPrivate: Boolean = shareDialog.isPrivate()
        val remoteName: String = shareDialog.getRemoteName()
        val description: String = shareDialog.getDescription()
        val account: GitCodeAccount = shareDialog.getAccount()!!

        object : Task.Backgroundable(project, GitCodeBundle.message("share.process")) {
            private lateinit var url: String

            override fun run(indicator: ProgressIndicator) {
                val token = GCCompatibilityUtil.getOrRequestToken(account, project) ?: return

                val requestExecutor = GitCodeApiRequestExecutor.Factory.getInstance().create(account.server, token)

                // create GitCode repo (network)
                LOG.info("Creating GitCode repository")
                indicator.text = GitCodeBundle.message("share.process.creating.repository")
                val result = requestExecutor.execute(
                    indicator,
                    GitCodeApiRequests.CurrentUser.Repos.create(account.server, name, description, isPrivate)
                )

                val repoRelust =
                    requestExecutor.execute(indicator, GitCodeApiRequests.Repos.get(account.server, account.name, name))
                url = repoRelust?.cloneUrl ?: return
                LOG.info("Successfully created GitCode repository")

                val root = gitRepository?.root ?: project.baseDir
                // creating empty git repo if git is not initialized
                LOG.info("Binding local project with GitCode")
                if (gitRepository == null) {
                    LOG.info("No git detected, creating empty git repo")
                    indicator.text = GitCodeBundle.message("share.process.creating.git.repository")
                    if (!createEmptyGitRepository(project, root)) {
                        return
                    }
                }

                val repositoryManager = GitUtil.getRepositoryManager(project)
                val repository = repositoryManager.getRepositoryForRoot(root)
                if (repository == null) {
                    GitCodeNotifications.showError(
                        project,
                        GitCodeNotificationIdsHolder.SHARE_CANNOT_FIND_GIT_REPO,
                        GitCodeBundle.message("share.error.failed.to.create.repo"),
                        GitCodeBundle.message("cannot.find.git.repo")
                    )
                    return
                }

                indicator.text = GitCodeBundle.message("share.process.retrieving.username")
                val username = accountInformationProvider.getInformation(requestExecutor, indicator, account).login
                val remoteUrl = gitHelper.getRemoteUrl(account.server, username, name)

                //git remote add origin git@gitcode.com:login/name.git
                LOG.info("Adding GitCode as a remote host")
                indicator.text = GitCodeBundle.message("share.process.adding.gh.as.remote.host")
                git.addRemote(repository, remoteName, remoteUrl).throwOnError()
                repository.update()

                // create sample commit for binding project
                if (!performFirstCommitIfRequired(project, root, repository, indicator, name, url)) {
                    return
                }

                //git push origin master
                LOG.info("Pushing to gitcode master")
                indicator.text = GitCodeBundle.message("share.process.pushing.to.gitcode.master")
                if (!pushCurrentBranch(project, repository, remoteName, remoteUrl, name, url)) {
                    return
                }

                GitCodeNotifications.showInfoURL(
                    project,
                    GitCodeNotificationIdsHolder.SHARE_PROJECT_SUCCESSFULLY_SHARED,
                    GitCodeBundle.message("share.process.successfully.shared"),
                    name,
                    url
                )
            }

            private fun createEmptyGitRepository(
                project: Project,
                root: VirtualFile
            ): Boolean {
                val result = Git.getInstance().init(project, root)
                if (!result.success()) {
                    VcsNotifier.getInstance(project).notifyError(
                        GitCodeNotificationIdsHolder.GIT_REPO_INIT_REPO,
                        GitBundle.message("initializing.title"),
                        result.errorOutputAsHtmlString
                    )
                    LOG.info("Failed to create empty git repo: " + result.errorOutputAsJoinedString)
                    return false
                }
                GitInit.refreshAndConfigureVcsMappings(project, root, root.path)
                GitUtil.generateGitignoreFileIfNeeded(project, root)
                return true
            }

            private fun performFirstCommitIfRequired(
                project: Project,
                root: VirtualFile,
                repository: GitRepository,
                indicator: ProgressIndicator,
                name: @NlsSafe String,
                url: String
            ): Boolean {
                // check if there is no commits
                if (!repository.isFresh) {
                    return true
                }

                LOG.info("Trying to commit")
                try {
                    LOG.info("Adding files for commit")
                    indicator.text = GitCodeBundle.message("share.process.adding.files")

                    // ask for files to add
                    val trackedFiles = ChangeListManager.getInstance(project).affectedFiles
                    val untrackedFiles =
                        filterOutIgnored(
                            project,
                            repository.untrackedFilesHolder.retrieveUntrackedFilePaths()
                                .mapNotNull(FilePath::getVirtualFile)
                        )
                    trackedFiles.removeAll(untrackedFiles) // fix IDEA-119855

                    val allFiles = ArrayList<VirtualFile>()
                    allFiles.addAll(trackedFiles)
                    allFiles.addAll(untrackedFiles)

                    val data = invokeAndWaitIfNeeded(indicator.modalityState) {
                        val dialog = GitCodeUntrackedFilesDialog(project, allFiles).apply {
                            if (trackedFiles.isNotEmpty()) {
                                selectedFiles = trackedFiles
                            }
                            DialogManager.show(this)
                        }

                        if (dialog.isOK && dialog.selectedFiles.isNotEmpty()) {
                            dialog.selectedFiles to dialog.commitMessage
                        } else {
                            null
                        }
                    }


                    if (data == null) {
                        GitCodeNotifications.showInfoURL(
                            project,
                            GitCodeNotificationIdsHolder.SHARE_EMPTY_REPO_CREATED,
                            GitCodeBundle.message("share.process.empty.project.created"),
                            name, url
                        )
                        return false
                    }
                    val (files2commit, commitMessage) = data

                    val files2add = ContainerUtil.intersection(untrackedFiles, files2commit)
                    val files2rm = ContainerUtil.subtract(trackedFiles, files2commit)
                    val modified = HashSet(trackedFiles)
                    modified.addAll(files2commit)

                    GitFileUtils.addFiles(project, root, files2add)
                    GitFileUtils.deleteFilesFromCache(project, root, files2rm)

                    // commit
                    LOG.info("Performing commit")
                    indicator.text = GitCodeBundle.message("share.process.performing.commit")
                    val handler = GitLineHandler(project, root, GitCommand.COMMIT)
                    handler.setStdoutSuppressed(false)
                    handler.addParameters("-m", commitMessage)
                    handler.endOptions()
                    Git.getInstance().runCommand(handler).throwOnError()

                    VcsFileUtil.markFilesDirty(project, modified)
                } catch (e: VcsException) {
                    LOG.warn(e)
                    GitCodeNotifications.showErrorURL(
                        project, GitCodeNotificationIdsHolder.SHARE_PROJECT_INIT_COMMIT_FAILED,
                        GitCodeBundle.message("share.error.cannot.finish"),
                        GitCodeBundle.message("share.error.created.project"),
                        " '$name' ",
                        GitCodeBundle.message("share.error.init.commit.failed") + GitCodeUtil.getErrorTextFromException(
                            e
                        ),
                        url
                    )
                    return false
                }

                LOG.info("Successfully created initial commit")
                return true
            }

            private fun filterOutIgnored(project: Project, files: Collection<VirtualFile>): Collection<VirtualFile> {
                val changeListManager = ChangeListManager.getInstance(project)
                val vcsManager = ProjectLevelVcsManager.getInstance(project)
                return ContainerUtil.filter(files) { file ->
                    !changeListManager.isIgnoredFile(file) && !vcsManager.isIgnored(
                        file
                    )
                }
            }

            private fun pushCurrentBranch(
                project: Project,
                repository: GitRepository,
                remoteName: String,
                remoteUrl: String,
                name: String,
                url: String
            ): Boolean {
                val currentBranch = repository.currentBranch
                if (currentBranch == null) {
                    GitCodeNotifications.showErrorURL(
                        project, GitCodeNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
                        GitCodeBundle.message("share.error.cannot.finish"),
                        GitCodeBundle.message("share.error.created.project"),
                        " '$name' ",
                        GitCodeBundle.message("share.error.push.no.current.branch"),
                        url
                    )
                    return false
                }
                val result = git.push(repository, remoteName, remoteUrl, currentBranch.name, true)
                if (!result.success()) {
                    GitCodeNotifications.showErrorURL(
                        project, GitCodeNotificationIdsHolder.SHARE_PROJECT_INIT_PUSH_FAILED,
                        GitCodeBundle.message("share.error.cannot.finish"),
                        GitCodeBundle.message("share.error.created.project"),
                        " '$name' ",
                        GitCodeBundle.message("share.error.push.failed", result.errorOutputAsHtmlString),
                        url
                    )
                    return false
                }
                return true
            }

            override fun onThrowable(error: Throwable) {
                GitCodeNotifications.showError(
                    project,
                    GitCodeNotificationIdsHolder.SHARE_CANNOT_CREATE_REPO,
                    GitCodeBundle.message("share.error.failed.to.create.repo"), error
                )
            }
        }.queue()
    }
}
